package net.oschina.allchat.message.processing.json.translators.convention;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;

import net.oschina.allchat.message.processing.BadMessageException;
import net.oschina.allchat.message.processing.ITranslator;
import net.oschina.allchat.message.processing.convention.annotations.BindTo;
import net.oschina.allchat.message.processing.convention.annotations.NotBind;
import net.oschina.allchat.message.processing.convention.annotations.Text;
import net.oschina.allchat.protocol.Constants;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

/**
 * <p>translate Object to json-string by reflection</p>
 * 
 * @author yiocio
 */

public class NamingConventionTranslator<T> implements ITranslator<T> {

	@Override
	public String translate(T obj) {
		ObjectMapper objectMapper = new ObjectMapper();
		JsonGenerator writer = null;
		StringWriter sw = new StringWriter();
		objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
		objectMapper.setSerializationInclusion(Include.NON_EMPTY);
		
		try {
			writer = objectMapper.getFactory().createGenerator(sw);
			doTranslate(obj,writer);
		} catch (Exception e) {
			throw new RuntimeException("failed to translate object to json", e);
		} finally {
			try {
				writer.close();
			} catch (Exception e) {
			}
		}
		
		return sw.getBuffer().toString();
	}

	/**
	 * the actual translation is done by this function
	 * 
	 * @param obj the object to translate to json string.This obj must NOT be array,primitive or void for now.
	 * @param writer the json serializer
	 * @throws Exception
	 */
	private void doTranslate(Object obj,JsonGenerator writer) throws Exception {
		Class<?> objType = obj.getClass();
		writer.writeStartObject(); 
		
		for (Field field : objType.getDeclaredFields()) {
			PropertyDescriptor propertyDescriptor = null;
			try {
				propertyDescriptor = new PropertyDescriptor(field.getName(), objType);
			} catch (IntrospectionException e) {
				continue;
			}
			
			if (propertyDescriptor.getReadMethod() == null || propertyDescriptor.getWriteMethod() == null)
				continue;
			
			if (field.getAnnotation(NotBind.class) != null) {
				continue;
			}
			
			Text text = field.getAnnotation(Text.class);
			BindTo bindTo = field.getAnnotation(BindTo.class);
			
			String name = null;
			if (bindTo != null) {
				name = bindTo.value();
			}
			
			if (name == null) {
				if (text != null) {
					name = Constants.SYMBOL_FIELD_TEXT;
				} else {
					name = getNameByNamingConvention(field.getName());
				}
			}
				
			Class<?> fieldType = field.getType();
			Method reader = propertyDescriptor.getReadMethod();
			if (isTextField(text, fieldType) || fieldType.equals(String.class)) {
				writer.writeStringField(name, (String)reader.invoke(obj));
			} else if (fieldType.equals(int.class) || fieldType.equals(Integer.class)) {
				writer.writeNumberField(name, (Integer)reader.invoke(obj));
			} else if (fieldType.equals(float.class) || fieldType.equals(Float.class)) {
				writer.writeNumberField(name, (Float)reader.invoke(obj));
			} else if (fieldType.equals(boolean.class) || fieldType.equals(Boolean.class)) {
				writer.writeBooleanField(name, (Boolean)reader.invoke(obj));
			} else if (List.class.isAssignableFrom(fieldType)) {
				Object objArray = reader.invoke(obj);
				writer.writeArrayFieldStart(name);
				for(Object item : (List<?>)objArray){
					if(isPrimitiveType(item.getClass())){
						if (fieldType.equals(String.class)) {
							writer.writeString((String)item);
						} else if (fieldType.equals(int.class) || fieldType.equals(Integer.class)) {
							writer.writeNumber((Integer)item);
						} else if (fieldType.equals(float.class) || fieldType.equals(Float.class)) {
							writer.writeNumber((Float)item);
						} else if (fieldType.equals(boolean.class) || fieldType.equals(Boolean.class)) {
							writer.writeBoolean((Boolean)item);
						}
					}else{
						doTranslate(item,writer);
					}
				}
				writer.writeEndArray();
			} else { // now,this filed should be a object
				Object o = reader.invoke(obj);
				writer.writeFieldName(name);
				doTranslate(o,writer);
			}
		}
		
		writer.writeEndObject(); 
	}
	
	private  boolean isPrimitiveType(Class<?> fieldType) {
		if(fieldType.isPrimitive()){
			return true;
		}
		
		if(fieldType.equals(String.class)){
			return true;
		}
		
		// to check whether it's WrapClass
		try { 
           return ((Class<?>) fieldType.getField("TYPE").get(null)).isPrimitive();
        } catch (Exception e) { 
            return false; 
        } 
	}
	
	private boolean isTextField(Text text, Class<?> fieldType) {
		if (text != null) {
			if (!fieldType.equals(String.class)) {
				throw new BadMessageException("text field must be string type");
			}
			
			return true;
		} else {
			return false;
		}
	}
	
	private String getNameByNamingConvention(String name) {
		StringBuilder sb = new StringBuilder();
		for (int i = 0 ; i < name.length(); i++) {
			char c = name.charAt(i);
			if (Character.isUpperCase(c)) {
				sb.append('-').append(Character.toLowerCase(c));
			} else {
				sb.append(c);
			}
		}
		
		if (sb.length() > 1 && sb.charAt(0) == '-') {
			sb.delete(0, 1);
		}
		
		return sb.toString();
	}
}
